import logging
from const import TYPEID_NONE
try:
    from dust.stats import MakeStimsHandleModuleCallbackContext
    from dust.stats import MakeStimsCalculateEffectsContext
    from dust.stats import MakeStimsCalculateDiffsContext
except ImportError:
    import contextlib

    @contextlib.contextmanager
    def ResourceTracking(Name = None):
        yield None



    def MakeDummyContext(*args, **kwargs):
        return ResourceTracking(*args, **kwargs)


    MakeStimsHandleModuleCallbackContext = MakeStimsCalculateEffectsContext = MakeStimsCalculateDiffsContext = MakeDummyContext
SLOTTYPE_VEHICLE_HIGH = 'VH'
SLOTTYPE_VEHICLE_LOW = 'VL'
SLOTTYPE_INFANTRY_HIGH = 'IH'
SLOTTYPE_INFANTRY_LOW = 'IL'
SLOTTYPE_IMPLANT = 'I'
SLOTTYPE_HANDHELD_WEAPON = 'WE'
SLOTTYPE_SMALL_WEAPON = 'A'
SLOTTYPE_MEDIUM_WEAPON = 'B'
SLOTTYPE_LARGE_WEAPON = 'C'
SLOTTYPE_EXTRA_LARGE_WEAPON = 'D'
SLOTTYPE_LARGE_OMS = 'O'
SLOTTYPE_MEDIUM_OMS = 'M'
SLOTTYPE_SMALL_OMS = 'S'
SLOTTYPE_RESERVED = 'R'
slotTypes = {SLOTTYPE_VEHICLE_HIGH: 'Vehicle High-Powered Module',
 SLOTTYPE_VEHICLE_LOW: 'Vehicle Low-Powered Module',
 SLOTTYPE_INFANTRY_HIGH: 'Infantry High-Powered Module',
 SLOTTYPE_INFANTRY_LOW: 'Infantry Low-Powered Module',
 SLOTTYPE_IMPLANT: 'Implant',
 SLOTTYPE_HANDHELD_WEAPON: 'Handheld Weapon',
 SLOTTYPE_SMALL_WEAPON: 'Small Weapon',
 SLOTTYPE_MEDIUM_WEAPON: 'Medium Weapon',
 SLOTTYPE_LARGE_WEAPON: 'Large Weapon',
 SLOTTYPE_EXTRA_LARGE_WEAPON: 'Extra Large Weapon',
 SLOTTYPE_LARGE_OMS: 'Large Off Map Support Module (Commander)',
 SLOTTYPE_MEDIUM_OMS: 'Medium Off Map Support Module (Squadron Leader)',
 SLOTTYPE_SMALL_OMS: 'Small Off Map Support Module (Infantry)',
 SLOTTYPE_RESERVED: 'Reserved Module'}
STATE_ONFIT = 1001
STATE_ONUNFIT = 1002
STATE_IDLE = 2001
STATE_ACTIVATING = 2002
STATE_ACTIVE = 2003
STATE_CHARGING = 2004
STATE_ALL = (STATE_IDLE,
 STATE_ACTIVATING,
 STATE_ACTIVE,
 STATE_CHARGING)
TINY_TIME = 0.001
PASSIVE = 'PASSIVE'
ACTIVE = 'ACTIVE'
MODULE_ATT_TYPES = (PASSIVE, ACTIVE)
MOD_BASE = 'BASE'
MOD_MUL = 'MULTIPLY'
MOD_ADD = 'ADD'
MOD_ALL = (MOD_BASE,
 MOD_MUL,
 MOD_ADD)
MOD_ACTIVE = 'A'
MOD_PASSIVE = 'P'
MOD_FUNCTION_ALL = (MOD_ACTIVE, MOD_PASSIVE)

class Adapter(object):
    numAdapter = 0
    __slots__ = ['sequencer',
     'typeName',
     'slots',
     'attribBase',
     'attribPassive',
     'attribActive',
     'uniqueName',
     'cppPointer']

    def __init__(self, typeName, fittingInfo, attributes, uniqueName = None):
        self.sequencer = None
        self.typeName = typeName
        self.cppPointer = None
        bogus = (set(fittingInfo) - set(slotTypes))
        if bogus:
            raise RuntimeError(("Adapter: 'fittingInfo' contains bogus slot type(s): %s" % list(bogus)))
        self.slots = [ [slotType, None] for slotType in fittingInfo ]
        if not isinstance(attributes, dict):
            raise RuntimeError("Adapter: 'attributes' must be a dict")
        self.attribBase = attributes
        self.attribPassive = {}
        self.attribActive = {}
        if not uniqueName:
            uniqueName = ('%s_%s' % (typeName, Adapter.numAdapter))
        Adapter.numAdapter += 1
        self.uniqueName = uniqueName



    def __repr__(self):
        return ("Adapter<name='%s', type='%s', slots=%s>" % (self.uniqueName,
         self.typeName,
         len(self.slots)))



    def _FitModule(self, module, slotIndex = None):
        if module.adapter:
            raise RuntimeError("_FitModule: This module already fitted. Can't be refitted")
        if (slotIndex is None):
            for (i, (slotType, fitted,),) in enumerate(self.slots):
                if ((module.slotType == slotType) and (fitted is None)):
                    slotIndex = i
                    break

            if (slotIndex is None):
                raise RuntimeError(('_FitModule: No slot available to fit this module[%s].' % module))
        else:
            (slotType, fitted,) = self.slots[slotIndex]
            if (module.slotType != slotType):
                raise RuntimeError(('_FitModule: The slot [%s:%s] is incompatible with %s.' % (slotType,
                 slotIndex,
                 module.slotType)))
            if fitted:
                self._UnfitModule(fitted)
        self.slots[slotIndex][1] = module
        module.slotIndex = slotIndex
        module.OnFitted(self)



    def _UnfitModule(self, module):
        if (module.function == MOD_ACTIVE):
            module.Deactivate()
        self.slots[module.slotIndex][1] = None
        module.OnUnfitted()



    def ClearModules(self):
        for (i, (slotType, fitted,),) in enumerate(self.slots):
            self.UnfitModule(i)




    def AutoFitModules(self, modules):
        slotsByTypeID = {}
        for (slotIndex, (slotType, module,),) in enumerate(self.slots):
            if module:
                moduleTypeID = module.typeID
                if (moduleTypeID not in slotsByTypeID):
                    slotsByTypeID[moduleTypeID] = [slotIndex]
                else:
                    slotsByTypeID[moduleTypeID].append(slotIndex)

        for newModule in modules:
            if (newModule.typeID in slotsByTypeID):
                del slotsByTypeID[newModule.typeID][-1]
                if not len(slotsByTypeID[newModule.typeID]):
                    del slotsByTypeID[newModule.typeID]
                else:
                    continue
            try:
                self.FitModule(newModule)
            except RuntimeError:
                logging.root.exception('Failed to fit type %s in slot %s', newModule.typeID, newModule.slotType)

        for (moduleTypeID, slotIndexes,) in slotsByTypeID.items():
            for slotIndex in slotIndexes:
                self.UnfitModule(slotIndex)





    def FitModule(self, module, slotIndex = None):
        self.FitModules([[module, slotIndex]])
        return module



    def FitModules(self, moduleList):
        try:
            for (module, slotIndex,) in moduleList:
                if module:
                    self._FitModule(module, slotIndex)
                else:
                    self._UnfitModule(self.slots[slotIndex][1])


        finally:
            self.CalculateEffects(PASSIVE)




    def UnfitModule(self, slotIndex):
        module = self.slots[slotIndex][1]
        if module:
            self._UnfitModule(module)
            self.CalculateEffects(PASSIVE)
            return module



    def TriggerState(self, isActivate, slotIndex):
        module = self.slots[slotIndex][1]
        if not module:
            raise RuntimeError(("TriggerState: There's no module in slot %s" % slotIndex))
        if isActivate:
            module.Activate()
        else:
            module.Deactivate()



    def GetSlotNum(self):
        return len(self.slots)



    def CalculateEffects(self, effect):
        changes = {}
        modules = [ module for (slotType, module,) in self.slots if module ]
        if (effect == PASSIVE):
            self.attribPassive.clear()
        else:
            self.attribActive.clear()
        for operator in [MOD_BASE,
         MOD_ADD,
         MOD_MUL]:
            for module in modules:
                attributeNames = module.GetAttributeNamesByOperator(operator)
                for attName in attributeNames:
                    attValue = self.attribBase[attName]
                    for bias in ([None] if (operator == MOD_BASE) else ['+', '-']):
                        if (effect == PASSIVE):
                            calcValue = self.attribPassive.get(attName, attValue)
                        elif (module.state != STATE_ACTIVE):
                            continue
                        calcValue = self.attribActive.get(attName, self.attribPassive.get(attName, attValue))
                        newCalcValue = module.Calculate(effect, attName, calcValue, bias, operator)
                        if (effect == PASSIVE):
                            self.attribPassive[attName] = newCalcValue
                        else:
                            self.attribActive[attName] = newCalcValue




        if (effect == PASSIVE):
            self.CalculateEffects(ACTIVE)



    def GetAttributeValue(self, attName):
        base = self.attribBase[attName]
        passive = self.attribPassive.get(attName, base)
        active = self.attribActive.get(attName, passive)
        return (base,
         passive,
         active)



    def GetAttributeValues(self, attName):
        d = {}
        for attName in self.attribBase.keys():
            d[attName] = self.GetAttributeValue(attName)

        return d



    def GetFittedModuleType(self, slotIndex):
        if self.slots[slotIndex][1]:
            return self.slots[slotIndex][1].typeID
        else:
            return TYPEID_NONE



    def RemoveFromSequencer(self):
        self.sequencer.RemoveAdapter(self)



    def GetSlotsInfo(self):
        return self.slots



    def GetModuleList(self):
        modulesList = []
        for slot in self.slots:
            typeID = (slot[1].typeID if slot[1] else TYPEID_NONE)
            modulesList.append(str(typeID))

        moduleListStr = ','.join(modulesList)
        return moduleListStr



    def DbgPrint(self):
        print self
        print ''
        print 'Attributes:'
        for attName in self.attribBase.keys():
            (base, passive, active,) = self.GetAttributeValue(attName)
            print ('   %s: %.1f -> %.1f = %1.f' % (attName,
             base,
             passive,
             active))

        print ''
        print 'Fitting:'
        for (slotType, module,) in self.slots:
            print ('   [%s]\t%s' % (slotType, (module if module else '')))





class Module(object):
    __slots__ = ['typeID',
     'slotType',
     'function',
     'attributes',
     'attributeNames',
     'isTogglable',
     'activationTime',
     'duration',
     'chargeTime',
     'cycles',
     'state',
     'currentCycle',
     'adapter',
     'slotIndex']

    def __init__(self, parameters, attributes):
        self.typeID = parameters['typeID']
        slotType = parameters['slotType']
        if (slotType not in slotTypes):
            raise RuntimeError(("Module: 'slotType=%s' does not indicate a known slot type." % slotType))
        self.slotType = slotType
        self.function = parameters['function']
        if (self.function not in MOD_FUNCTION_ALL):
            raise RuntimeError("Module: 'function' must be 'A' or 'P'")
        self.attributes = {}
        self.attributeNames = {}
        for operator in MOD_ALL:
            self.attributes[operator] = []
            self.attributeNames[operator] = []

        for (attType, attName, attModifier, attValue,) in attributes:
            if (attModifier in MOD_ALL):
                self.attributes[attModifier].append((attType,
                 attName,
                 attValue))
            else:
                raise RuntimeError(('Module: invalid modifier during initialization: %s' % attModifier))

        self.isTogglable = False
        self.activationTime = parameters['activationTime']
        self.duration = parameters['duration']
        self.chargeTime = parameters['rechargeTime']
        self.cycles = parameters['cycles']
        self.state = STATE_IDLE
        self.currentCycle = 0
        self.adapter = None
        self.slotIndex = None



    def __repr__(self):
        return ("Module<'%s' (%s%s)>" % (self.typeID,
         self.slotType,
         self.function))



    def Activate(self):
        self.EnterState(STATE_ACTIVATING)



    def Deactivate(self):
        if (self.function != MOD_ACTIVE):
            raise RuntimeError(('Module::Deactivate: This module is not activateable: %s' % str(self)))
        if (self.state != STATE_IDLE):
            self.EnterState(STATE_CHARGING)



    def GetAttributeNamesByOperator(self, operator):
        if (operator not in self.attributeNames):
            raise RuntimeError('Module: Getting attribute names with invalid operator: %s', operator)
        return self.attributeNames[operator]



    def Calculate(self, effect, calcName, calcValue, bias, operator):
        attributesList = self.attributes[operator]
        for (attType, attName, attValue,) in attributesList:
            if (attName != calcName):
                continue
            if (attType != effect):
                continue
            if ((bias is not None) and (((bias == '+') and (attValue < 0.0)) or ((bias == '-') and (attValue > 0.0)))):
                continue
            originType = type(calcValue)
            if (operator == MOD_MUL):
                return originType((calcValue * attValue))
            if (operator == MOD_ADD):
                return originType((calcValue + attValue))
            if (operator == MOD_BASE):
                return originType(attValue)

        return calcValue



    def EnterState(self, gotoState):
        if (self.function != MOD_ACTIVE):
            raise RuntimeError(("Module::EnterState: This module doesn't support state transitions: %s" % str(self)))
        if (gotoState == self.state):
            return 
        if (gotoState == STATE_ACTIVATING):
            if (self.state != STATE_IDLE):
                return 
            self.state = STATE_ACTIVATING
            self.adapter.sequencer.QueueEvent(self)
            if (self.activationTime < TINY_TIME):
                return self.EnterState(STATE_ACTIVE)
            self.adapter.sequencer.Schedule(self, self.activationTime, 'enter active')
        elif (gotoState == STATE_ACTIVE):
            if (self.state != STATE_ACTIVATING):
                return 
            self.state = STATE_ACTIVE
            self.currentCycle = 0
            self.adapter.sequencer.QueueEvent(self)
            if (self.duration < TINY_TIME):
                if (self.duration == -1):
                    print 'STAY ACTIVE BIATCH'
                else:
                    return self.EnterState(STATE_CHARGING)
            else:
                duration = ((self.duration / self.cycles) if self.cycles else 1)
                self.adapter.sequencer.Schedule(self, duration, 'cycle')
        elif (gotoState == STATE_CHARGING):
            if (self.state == STATE_ACTIVE):
                self.state = STATE_CHARGING
                self.adapter.sequencer.QueueEvent(self)
                if (self.chargeTime > TINY_TIME):
                    self.adapter.sequencer.Schedule(self, self.chargeTime, 'enter idle')
                else:
                    return self.EnterState(STATE_IDLE)
            else:
                return self.EnterState(STATE_IDLE)
        else:
            if (gotoState == STATE_IDLE):
                if (self.state == STATE_CHARGING):
                    self.state = STATE_IDLE
                    self.adapter.sequencer.QueueEvent(self)



    def OnSchedule(self, event):
        if (event == 'enter active'):
            self.EnterState(STATE_ACTIVE)
        elif (event == 'cycle'):
            if (self.currentCycle < self.cycles):
                self.currentCycle += 1
                self.adapter.sequencer.Schedule(self, (self.duration / self.cycles), 'cycle')
                self.adapter.sequencer.QueueEvent(self)
            else:
                self.EnterState(STATE_CHARGING)
        elif (event == 'enter idle'):
            self.EnterState(STATE_IDLE)



    def OnFitted(self, adapter):
        if self.adapter:
            raise RuntimeError("Module::OnFitted: This module already fitted. Can't be refitted")
        for (operator, attributesList,) in self.attributes.iteritems():
            listIndex = 0
            attNames = self.attributeNames[operator]
            for (attType, attName, attValue,) in attributesList:
                if (attType not in MODULE_ATT_TYPES):
                    raise RuntimeError(("Module::OnFitted: 'This attribute has bogus type %s" % str(attType)))
                if (attName not in adapter.attribBase):
                    self.attributes.pop(listIndex)
                elif (attName not in attNames):
                    attNames.append(attName)
                listIndex += 1


        self.adapter = adapter
        if self.adapter.sequencer:
            self.adapter.sequencer.QueueEvent(self, STATE_ONFIT)



    def OnUnfitted(self):
        if self.adapter.sequencer:
            self.adapter.sequencer.QueueEvent(self, STATE_ONUNFIT)
        self.adapter = None




class Sequencer(object):

    def __init__(self):
        self.adapters = set()
        self.timeline = []
        self.eventqueue = []
        self.lastDiff = {}
        self.currentTime = 0.0



    def FlushAll(self):
        print 'FlushAll called in Sequencer'
        self.adapters = set()
        self.timeline = []
        self.eventqueue = []
        self.lastDiff = {}



    def UpdateTime(self, currentTime):
        self.currentTime = currentTime
        with MakeStimsHandleModuleCallbackContext():
            while (self.timeline and (self.timeline[0][0] < currentTime)):
                (timeStamp, module, event,) = self.timeline.pop(0)
                module.OnSchedule(event)

        activeEffects = []
        lastDiff = self.lastDiff
        self.lastDiff = {}
        for adapter in self.adapters:
            with MakeStimsCalculateEffectsContext():
                adapter.CalculateEffects(ACTIVE)
            with MakeStimsCalculateDiffsContext():
                activeDiff = (set(adapter.attribActive.items()) - set(adapter.attribPassive.items()))
                passiveDiff = (set(adapter.attribPassive.items()) - set(adapter.attribBase.items()))
                diff = (activeDiff | passiveDiff)
                diffAttList = []
                for (diffAttName, diffAttValue,) in diff:
                    diffAttList.append(diffAttName)

                if (diff != lastDiff.get(adapter, set())):
                    lastDiffSet = lastDiff.get(adapter, set())
                    listItems = []
                    for (attName, attValue,) in lastDiffSet:
                        if ((attName not in diffAttList) and (attName in adapter.attribPassive)):
                            listItems.append((attName, adapter.attribPassive[attName]))

                    activeEffects.append((adapter, listItems))
                    activeEffects.append((adapter, list(diff)))
                self.lastDiff[adapter] = diff

        tmp = self.eventqueue
        self.eventqueue = []
        self.lastActiveEffects = activeEffects
        return (tmp, activeEffects)



    def QueueEvent(self, module, state = None):
        if (state is None):
            state = module.state
        self.eventqueue.append((self.currentTime,
         state,
         module.currentCycle,
         module,
         module.adapter))



    def AddAdapter(self, adapter):
        self.adapters.add(adapter)
        adapter.sequencer = self



    def Schedule(self, module, inSeconds, event):
        self.timeline.append(((self.currentTime + inSeconds),
         module,
         event))
        self.timeline.sort()



    def Cancel(self, module):
        for event in self.timeline:
            pass




    def GetAdapter(self, adapterName):
        for adapter in self.adapters:
            if (adapter.uniqueName == adapterName):
                return adapter




    def RemoveAdapter(self, adapter):
        if (adapter in self.adapters):
            self.adapters.remove(adapter)




